﻿
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using MammothMicro.SilverSocket.Protocol.System;
using MammothMicro.SilverSocket.Shared;
using SilverSocket.Shared;

namespace MammothMicro.SilverSocket.Client
{
    /// <summary>
    /// Send and receives messages.
    /// </summary>
    public abstract class SocketClient : IMessageClient
    {
        private readonly IMessageCompressor _messageCompressor;

        private readonly IMessageEncryptor _messageEncryptor;

        private readonly int _port;

        private readonly IPEndPoint _remoteAddress;
        
        /// <summary>
        /// Used to aggregate message frames.
        /// </summary>
        private List<byte> _messageBuffer = new List<byte>();

        /// <summary>
        /// Serialize and deserialize messages.
        /// </summary>
        private IMessageSerializer _messageSerializer;
        
        private bool _encryptData;

        private bool _compressData;

        /// <summary>
        /// Socket which will be used for message transfer.
        /// </summary>
        private System.Net.Sockets.Socket socket;

        public SocketClient(IPEndPoint remoteAddress, IMessageSerializer messageSerializer, IMessageCompressor messageCompressor,
                            IMessageEncryptor messageEncryptor)
        {
            this._messageEncryptor = messageEncryptor;
            this._messageCompressor = messageCompressor;
            // this._port = port;
            this._remoteAddress = remoteAddress ;
            this._messageSerializer = messageSerializer;
            this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        /// <summary>
        /// Prevents a default instance of the <see cref="SocketClient"/> class from being created.
        /// </summary>
        private SocketClient()
        {
        }

   

        /// <summary>
        /// Occurs when client has connected to message server
        /// </summary>
        public event EventHandler<AsyncCompletedEventArgs> ConnectCompleted;

        /// <summary>
        /// Occurs if connection with message server has been lost.
        /// </summary>
        public event EventHandler<EventArgs> ConnectionLost;

        /// <summary>
        /// Occurs when new message have been received.
        /// </summary>
        public event EventHandler<MessageReceivedEventArgs> MessageRecieved;

        /// <summary>
        /// Occurs when message is sent
        /// </summary>
        public event EventHandler<AsyncCompletedEventArgs> SendCompleted;

        /// <summary>
        /// Indicates if client is connected to message server.
        /// </summary>
        public bool Connected
        {
            get
            {
                return this.socket.Connected;
            }
        }

        /// <summary>
        /// Connect to message server async.
        /// </summary>
        public void ConnectAsync()
        {
            this.ConnectAsync(null);
        }

        /// <summary>
        /// Connect to message server async.
        /// </summary>
        /// <param name="state">
        /// </param>
        public void ConnectAsync(object state)
        {
            if (!this.Connected)
            {
                var endPoint = new DnsEndPoint(this._remoteAddress.Address.ToString(), this._port);
                var args = new SocketAsyncEventArgs {UserToken = state, RemoteEndPoint = _remoteAddress};
                
                args.Completed += this.OnSocketConnected;
                this.socket.ConnectAsync(args);
            }
        }

        /// <summary>
        /// Disconnect from message server.
        /// </summary>
        public void Disconnect()
        {
            this.socket.Close();
        }

        /// <summary>
        /// Send message.
        /// </summary>
        /// <param name="message">
        /// </param>
        public void SendMessageAsync(DuplexMessage message)
        {
            this.SendMessageAsync(message, null);
        }

        /// <summary>
        /// Send message.
        /// </summary>
        /// <param name="message">
        /// </param>
        /// <param name="userState">
        /// </param>
        private void SendMessageAsync(DuplexMessage message, object userState)
        {
            if ((this.socket == null) || (this.Connected == false))
            {
                // TODO : throw exception.
            }

            var args = new SocketAsyncEventArgs();

            List<byte> messageByteList = this.PrepareMessage(message); 
            for (int i = 1023; i < messageByteList.Count; i += 1023)
            {
                messageByteList.Insert(i, 0);
                i++;
            }

             messageByteList.Add(1);

            args.SetBuffer(messageByteList.ToArray(), 0, messageByteList.Count);
            args.Completed += this.SocketSendCompleted;
            args.UserToken = userState;

            if ((this.socket != null) && (this.socket.Connected == true))
            {
                this.socket.SendAsync(args); 
            }
            else
            {
                // TODO try to reconnect
            }
            
        }

        /// <summary>
        /// Prepare message to be sent
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        private List<byte> PrepareMessage(DuplexMessage message)
        {
            var stream = this._messageSerializer.Serialize(message);

            if (this._encryptData)
                stream = this._messageEncryptor.Encrypt(stream);

            if (this._compressData)
                stream = this._messageCompressor.Compress(stream);

            return stream;
        }

        /// <summary>
        /// Process received message
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        private DuplexMessage ProcessMessage(IEnumerable<byte> stream)
        {
            List<byte> byteList = stream.ToList();
            if (this._compressData)
                byteList = this._messageCompressor.Decompress(byteList);

            if (this._encryptData)
                byteList = this._messageEncryptor.Decrypt(byteList);

            var obj = this._messageSerializer.Deserialize(byteList);
            return obj;
        }

        /// <summary>
        /// </summary>
        /// <param name="e">
        /// </param>
        private void OnConnected(AsyncCompletedEventArgs e)
        {
            EventHandler<AsyncCompletedEventArgs> handler = this.ConnectCompleted;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// </summary>
        /// <param name="e">
        /// </param>
        private void OnConnectionLost()
        {
            EventHandler<EventArgs> handler = this.ConnectionLost;
            if (handler != null)
            {
                handler(this, new EventArgs());
            }
        }

        /// <summary>
        /// </summary>
        /// <param name="e">
        /// </param>
        private void OnMessageRecieved(MessageReceivedEventArgs e)
        {
            EventHandler<MessageReceivedEventArgs> handler = this.MessageRecieved;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// </summary>
        /// <param name="e">
        /// </param>
        private void OnSendCompleted(AsyncCompletedEventArgs e)
        {
            EventHandler<AsyncCompletedEventArgs> handler = this.SendCompleted;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// </summary>
        /// <param name="sender">
        /// </param>
        /// <param name="e">
        /// </param>
        private void OnSocketConnected(object sender, SocketAsyncEventArgs e)
        {
            AsyncCompletedEventArgs asyncArgs;
            if (this.Connected)
            {
                var response = new byte[1024];
                e.SetBuffer(response, 0, response.Length);
                e.Completed -= this.OnSocketConnected;
                e.Completed += this.OnSocketReceive;

                this.socket.ReceiveAsync(e);
                asyncArgs = new AsyncCompletedEventArgs(null, false, e.UserToken);
                this.OnConnected(asyncArgs);
            }

            // TODO: Handle error while connection.
        }

        /// <summary>
        /// </summary>
        /// <param name="sender">
        /// </param>
        /// <param name="e">
        /// </param>
        private void OnSocketReceive(object sender, SocketAsyncEventArgs e)
        {
            if (e.BytesTransferred == 0)
            {
                this.OnConnectionLost();

                this.socket.Close();

                return;
            }

            this._messageBuffer.AddRange(e.Buffer.Take(e.BytesTransferred - 1));
            if (e.Buffer[e.BytesTransferred - 1] > 0)
            {
                this.OnMessageRecieved(new MessageReceivedEventArgs(string.Empty, this.ProcessMessage(this._messageBuffer)));
                this._messageBuffer.Clear();
            }

            this.socket.ReceiveAsync(e);
        }

        /// <summary>
        /// </summary>
        /// <param name="sender">
        /// </param>
        /// <param name="e">
        /// </param>
        private void SocketSendCompleted(object sender, SocketAsyncEventArgs e)
        {
            if (e.SocketError == SocketError.Success)
            {
                this.OnSendCompleted(new AsyncCompletedEventArgs(null, false, e.UserToken));
            }

            // TODO: Handle error if send is not successful.
        }
    }
}